// Copyright DustedPixels.com 2008. All rights reserved.

package com.dustedpixels.jasmin.cpu;

import java.util.logging.Logger;

import com.dustedpixels.common.Threads;
import com.dustedpixels.jasmin.common.Timeline;
import com.dustedpixels.jasmin.memory.Memory;

/**
 * @author micapolos@gmail.com (Michal Pociecha-Los)
 */
public final class CrewMemoryKit {
  // Initialize shared logger.
  private final static Logger LOGGER = Logger.getLogger(CrewMemoryKit.class.getName());
  
  // Synchronizes writer thread and reader threads.
  private final Object mutex;

  private int readersWaiting;
  private final BufferItem[] buffer;
  private int bufferSize;
  private long writerCycles; 

  private final EwMemory ewMemory;
  private final CrMemory[] crMemories;

  public CrewMemoryKit(Timeline[] crTimelines,
      Timeline ewTimeline,
      Memory[] crMemories,
      Memory ewMemory,
      int bufferSize) {
    
    // Initialize write-buffer.
    this.buffer = new BufferItem[bufferSize];
    for (int i = 0; i < buffer.length; i++) {
      buffer[i] = new BufferItem();
    }
    
    // Initialize data shared by writer and readers.
    // Access to these properties can be done only when synchronized
    // on the mutex.
    this.mutex = new Object();
    this.readersWaiting = 0;
    this.bufferSize = 0;
    this.writerCycles = ewTimeline.cycles();

    // Initialize memories.
    this.ewMemory = new EwMemory(ewTimeline, ewMemory);
    this.crMemories = new CrMemory[crTimelines.length];
    for (int i = 0; i < crMemories.length; i++) {
      this.crMemories[i] = new CrMemory(crTimelines[i], crMemories[i]);
    }
  }

  /**
   * Returns Exclusive-Write memory, which may be read/written in the scope of
   * Exclusive-Write timeline.
   */
  public Memory getEwMemory() {
    return ewMemory;
  }

  /**
   * Returns Concurrent-Read memories, which may be used in the scope of crTimelines.
   */
  public Memory[] getCrMemories() {
    return crMemories;
  }
  
  private final class EwMemory implements Memory {
    private final Memory localMemory;
    private final Timeline localTimeline;
    private int localBufferIndex;

    private EwMemory(Timeline timeline, Memory memory) {
      this.localTimeline = timeline;
      this.localMemory = memory;
      this.localBufferIndex = 0;
    }

    public byte read(int address) {
      return localMemory.read(address);
    }

    public void write(int address, byte value) {
      // If buffer is full, suspend writer thread and resume all reader threads.
      if (localBufferIndex == buffer.length) {
        sync(true);
      }

      // Write value to local memory
      localMemory.write(address, value);

      // Write value to the buffer
      BufferItem bufferItem = buffer[localBufferIndex++];
      bufferItem.cycles = localTimeline.cycles();
      bufferItem.address = address;
      bufferItem.value = value;
    }

    public void flush() {
      sync(false);
    }
    
    private void sync(boolean wait) {
      synchronized (mutex) {
        // Wake up all reader threads, and go to sleep.
        readersWaiting = 0;
        bufferSize = localBufferIndex;
        writerCycles = localTimeline.cycles();
        
        //LOGGER.fine("Waking up all readers");
        
        Threads.notifyAllOn(mutex);
        
        if (wait && readersWaiting != crMemories.length) {
          //LOGGER.fine("Supending writer.");
          Threads.waitOn(mutex);
          //LOGGER.fine("Writer woke up");
        }
      }

      localBufferIndex = 0;      
    }
  }

  private final class CrMemory implements Memory {
    private final Memory localMemory;
    private final Timeline localTimeline;
    private int localBufferIndex;
    private int localBufferSize;
    private long localWriterCycles;

    private CrMemory(Timeline timeline, Memory memory) {
      this.localTimeline = timeline;
      this.localMemory = memory;
      this.localBufferIndex = 0;
      this.localBufferSize = bufferSize;
      this.localWriterCycles = writerCycles;
    }

    public byte read(int address) {
      // If timeline of this thread passed timeline of writer thread, suspend this thread.
      if (localTimeline.cycles() >= localWriterCycles) {
        synchronized (mutex) {
          while (localTimeline.cycles() >= writerCycles) {            
            if (readersWaiting == crMemories.length - 1) {
              //LOGGER.fine("Waking up the writer");
              Threads.notifyOn(mutex);
            }
            
            readersWaiting++;
            //LOGGER.fine("Suspending reader");
            Threads.waitOn(mutex);
            //LOGGER.fine("Reader woke up");
            localWriterCycles = writerCycles;
            localBufferSize = bufferSize;
          }
        }
        
        localBufferIndex = 0;
      }

      while (localBufferIndex < localBufferSize) {
        BufferItem bufferItem = buffer[localBufferIndex];
        if (bufferItem.cycles > localTimeline.cycles())
          break;

        localMemory.write(bufferItem.address, bufferItem.value);
        localBufferIndex++;
      };

      return localMemory.read(address);
    }

    public void write(int address, byte value) {
      // No-op
    }

    public void flush() {
      // No-op
    }
  }

  /**
   * Describes write operation performed at given moment in time.
   */
  private final class BufferItem {
    private long cycles;
    private int address;
    private byte value;
  }
  
  public static void main(String[] args) throws Exception {
    int count = 1;
    int steps = 3500000;
    
    Timeline rwTimeline = new TimelineImpl();
    Memory rwMemory = new MemoryImpl(16);
    
    Timeline[] roTimelines = new Timeline[count];
    Memory[] roMemories = new Memory[count];
    for (int i = 0; i < count; i++) {
      roTimelines[i] = new TimelineImpl();
      roMemories[i] = new MemoryImpl(16);
    }
    
    CrewMemoryKit memoryKit = new CrewMemoryKit(
        roTimelines,
        rwTimeline,
        roMemories,
        rwMemory,
        10240);
    
    Thread rwThread = new Thread(new MemoryWriter(rwTimeline, memoryKit.getEwMemory(), steps));
    rwThread.start();
    
    Thread[] roThreads = new Thread[count];
    for (int i = 0; i < count; i++) {
      roThreads[i] = new Thread(new MemoryReader(roTimelines[i], memoryKit.getCrMemories()[i], steps));
      roThreads[i].start();
    }
    
    rwThread.join();
    
    for (int i = 0; i < count; i++) {
      roThreads[i].join();
    }
  }
}
